/*
 * Flashlib - QB9 common Flash library
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.qb9.flashlib.input
{
	import com.qb9.flashlib.color.Color;
	import com.qb9.flashlib.easing.Tween;
	import com.qb9.flashlib.interfaces.IDisposable;
	import com.qb9.flashlib.interfaces.IMoveable;
	import com.qb9.flashlib.tasks.Func;
	import com.qb9.flashlib.tasks.ITask;
	import com.qb9.flashlib.tasks.Sequence;
	import com.qb9.flashlib.tasks.TaskContainer;
	import com.qb9.flashlib.utils.ArrayUtil;
	
	import flash.display.InteractiveObject;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.geom.Rectangle;
	import flash.text.TextField;
	import flash.text.TextFieldType;
	import flash.text.TextFormat;
	import flash.utils.setTimeout;

	/**
	 * Visual console class that allows the registration of custom commands.
	 * 
	 * Built-in commands:
	 *   - clear: clears the history for both text fields
	 *   - cmds: dumps the list of commands that contain a prefix or all if none is provided
	 */

	public class Console extends Sprite implements IDisposable, IMoveable
	{
		private static const FIELD_HEIGHT:uint = 20;
		private static const CURSOR:String = '>> ';
		private static const DEFAULT_IN_RATIO:Number = 0.75;
		
		private static const BG_COLOR:uint = 0x222222;
		private static const DEFAULT_LOG_COLOR:uint = 0x999999;
		private static const BORDER_COLOR:uint = 0xAAAAAA;
		
		private static const FIELD_FONT_SIZE:uint = 11;
		private static const HISTORY_FONT_SIZE:uint = 10;
		
		private static const SLIDE_DURATION:uint = 750;
		
		private var area:Rectangle;
		
		private var tasks:TaskContainer;
		private var task:ITask;
		
		private var history:TextField;
		private var field:TextField;
		
		private var hotkey:Hotkey;
		private var oldFocus:InteractiveObject;
		
		private var cmds:Object = {};
		private var stack:Array = [''];
		private var ptr:int = 0;
		
		public function Console(tasks:TaskContainer=null, area:Rectangle=null)
		{
			this.tasks = tasks;
			this.area = area;
			
			makeInvisible();
			addEventListener(Event.ADDED_TO_STAGE, generate);
		}
		
		private function generate(e:Event):void
		{
			removeEventListener(Event.ADDED_TO_STAGE, generate);
			
			area = area || new Rectangle(0,0, stage.stageWidth, stage.stageHeight * DEFAULT_IN_RATIO);
			
			init();
			createHistory();
			createField();
			
			hotkey = new Hotkey(this);
			hotkey.add('enter', flush);
			hotkey.add('tab', autocomplete);
			hotkey.add('up', stackUp);
			hotkey.add('down', stackDown);
			hotkey.add('esc', _hide);
		}
		
		private function init():void
		{
			graphics.beginFill(BG_COLOR, 0.9);
			graphics.drawRect(0,0, area.width, area.height);
			graphics.endFill();
			
			y = -area.height;
			
			addBuiltIn();
		}
		
		private function addBuiltIn():void
		{
			register('clear', clear);
			register('cmds', cmdList);
		}
		
		private function cmdList(cmd:String=''):String
		{
			return suggestions(cmd).join(' \t') || 'No match found';
		}
		
		private function arity(cmd:String):int
		{
			return cmds[cmd].length;
		}
		
		private function makeField(color:uint, size:uint):TextField
		{
			var f:TextField = new TextField();
			// Override attributes that could be changed when added to stage
			addChild(f);
			f.embedFonts = false;
			f.width = area.width - 2;
			f.borderColor = BORDER_COLOR;
			f.border = true;
			f.defaultTextFormat = makeFormat(color, size);
			return f;
		}
		
		private function makeFormat(color:uint, size:uint):TextFormat
		{
			var fmt:TextFormat = new TextFormat('Verdana', size, color);
			fmt.leftMargin = 10;
			fmt.leading = 2;
			return fmt;
		}
		
		private function createHistory():void
		{
			history = makeField(DEFAULT_LOG_COLOR, HISTORY_FONT_SIZE);
			history.y = 1;
			history.wordWrap = true;
			history.height = area.height - FIELD_HEIGHT - 5;
		}
		
		private function createField():void
		{
			field = makeField(Color.WHITE, FIELD_FONT_SIZE);
			field.type = TextFieldType.INPUT;
			field.y = area.height - FIELD_HEIGHT - 1;
			field.height = FIELD_HEIGHT;
		}
		
		private function log(text:String):void
		{
			history.appendText('\n' + text);
			history.scrollV = history.maxScrollV;
		}
		
		private function flush(e:Event):void
		{
			var cmd:String = field.text;
			if (!cmd)
				return;
			
			run(cmd);
			
			ArrayUtil.removeElement(stack, cmd);
			stack.splice(1, 0, cmd);

			readFromStack(0);
		}
		
		public function register(cmd:String, fn:Function):void
		{
			cmds[cmd] = fn;
		}
		
		public function unregister(cmd:String):void
		{
			delete cmds[cmd];
		}
		
		public function run(command:String):void
		{
			coloredLog(command, Color.YELLOW);
			
			var parser:Command = new Command(command);
			var cmd:String = parser.readWord();
			var fn:Function = cmds[cmd];
			
			if (fn === null)
				return error(cmd+': command not found');
			
			var args:Array = parser.readArgs();

			try	{
				var ret:Object = fn.apply(null, args);
				
				if (ret !== null)
					print(ret);
				
			}catch(err:ArgumentError){
				error('wrong amount of arguments, expected: '+arity(cmd));
			}catch(err:Error){
				error(err.message);
			}
		}
		
		private function error(msg:String):void
		{
			coloredLog(msg, Color.RED);
		}
		
		public function print(obj:Object, color:uint=DEFAULT_LOG_COLOR):void
		{
			var msg:String = (CURSOR+obj).replace(/(\r?\n)/g, '$1'+CURSOR);
			coloredLog(msg, color);
		}
		
		private function coloredLog(msg:String, color:uint):void
		{
			var start:int = history.text.length;
			msg += ' ';
			log(msg);
			history.setTextFormat(makeFormat(color, HISTORY_FONT_SIZE), start, start+msg.length);
		}
		
		private function suggestions(str:String):Array
		{
			var list:Array = [];
			for (var cmd:String in cmds)
				if (cmd.indexOf(str) === 0)
					list.push(cmd);

			return list.sort();
		}
		
		private function fieldText(text:String):void
		{
			field.text = text;
			fixCursor();
		}
		
		private function autocomplete(e:Event):void
		{
			if (!showing)
				return;
			
			focusField();
			
			var cmd:String = field.text;
			if (cmd.indexOf(' ') !== -1)
				return;
			
			var list:Array = suggestions(cmd);
			var fst:String = list[0];
			
			switch (list.length)
			{
				case 0: break;
				case 1:	fieldText(fst+' '); break;
				default:
					while (true)
					{
						var c:String = fst.charAt(cmd.length);
						if (!c || suggestions(cmd+c).length !== list.length)
							break;
						cmd += c;
					}

					if (cmd !== field.text)
						fieldText(cmd);
					else
						print(cmdList(cmd));					
				break;
			}
		}
		
		private function fixCursor():void
		{
			if (field === null) // disposed
				return;

			var i:int = field.text.length;
			field.setSelection(i,i);
		}
		
		private function _hide(e:Event):void
		{
			hide();
		}
		
		public function get moving() : Boolean
		{
			return task && task.running;	
		}
		
		public function get showing() : Boolean
		{
			return y !== -area.height;	
		}
		
		private function focusElem(elem:InteractiveObject):void
		{
			if (stage && stage.focus !== elem)
				stage.focus = elem;
		}
		
		public function hide():void
		{
			if (moving)
				return;

			focusElem(oldFocus || stage);
			moveTo(-area.height, true);
		}
		
		public function show():void
		{
			if (moving)
				return;
			
			visible = true;

			if (stage.focus && stage.focus !== field)
				oldFocus = stage.focus; 

			moveTo(0);
			focusField();
		}
		
		private function focusField():void
		{
			delay(focusElem, field);
		}
		
		public function toggle():void
		{
			if (showing)
				hide();
			else
				show();
		}
		
		private function makeInvisible():void
		{
			visible = false;
		}
		
		private function moveTo(pos:int, hide:Boolean=false):void
		{
			task = new Tween(this, SLIDE_DURATION, {y:pos}, {transition:'easeOut'});
			
			if (hide)
				task = new Sequence(task, new Func(makeInvisible));
			
			// Instant movement when tasks wasn't provided
			if (tasks)
			{
				tasks.add(task);
			}
			else
			{
				task.start();
				task.update(SLIDE_DURATION);
			}
		}
		
		public function clear():void
		{
			history.text = '';
			delay(clearStack);
		}
		
		private function clearStack():void
		{
			if (stack)
				stack.length = 1;
		}
		
		private function stackUp(e:Event):void
		{
			readFromStack(ptr+1);
		}
		
		private function stackDown(e:Event):void
		{
			readFromStack(ptr-1);
		}
		
		private function readFromStack(i:int):void
		{
			if (i in stack === false)
				return;
			
			ptr = i;
			fieldText(stack[i]);
			delay(fixCursor);
		}
		
		private function delay(fn:Function, ...args:Array):void
		{
			setTimeout.apply(null, [fn, 1].concat(args));
		}
		
		public function dispose():void
		{
			cmds = null;
			task = tasks = null;
			oldFocus = field = history = null;
			stack = null;
			
			if (hotkey)
				hotkey.dispose();
			hotkey = null;
		}
	}
}
	import com.qb9.flashlib.config.Evaluator;
	

internal class Command 
{
	private static const SPACE:String = ' ';
	private static const QUOTE:String = '"';
	
	private var data:String;
	
	public function Command(data:String)
	{
		this.data = data;
	}
	
	public function readWord() : String
	{
		return until(SPACE);
	} 
	
	private function index(chr:String):int
	{
		var i:int = data.indexOf(chr, 1);
		if (i === -1)
			i = data.length;
		return i;
	}
	
	private function until(chr:String):String
	{
		var i:int = index(chr);
		var ret:String = data.slice(0,i);
		data = data.slice(i+1);
		return ret;
	}
	
	public function readArgs():Array
	{
		var ret:Array = [];
		
		while (data)
		{
			if (data.charAt(0) === SPACE)
				data = data.slice(1);
			else
				ret.push(readToken());
		}

		return ret;
	}
	
	private function readToken():Object
	{
		// literal
		if (data.charAt(0) === QUOTE)
			return until(QUOTE).slice(1);

		return Evaluator.parse(readWord());
	}
}